package city.spring.configure;

import city.spring.modules.log.entity.ExceptionLogEntity;
import city.spring.modules.log.service.ExceptionLogService;
import city.spring.utils.UserUtils;
import com.baomidou.mybatisplus.extension.api.ApiController;
import com.baomidou.mybatisplus.extension.api.IErrorCode;
import com.baomidou.mybatisplus.extension.api.R;
import org.apache.catalina.connector.ClientAbortException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 全局异常捕获处理程序
 *
 * @author HouKunLin
 */
@RestControllerAdvice
public class RestControllerExceptionHandler extends ApiController {
    private static final Logger logger = LoggerFactory.getLogger(RestControllerExceptionHandler.class);
    private final HttpServletRequest request;
    private final ExceptionLogService exceptionLogService;

    public RestControllerExceptionHandler(HttpServletRequest request, ExceptionLogService exceptionLogService) {
        this.request = request;
        this.exceptionLogService = exceptionLogService;
    }

    /**
     * 严重的错误，不在 @ExceptionHandler 捕获名单里面的错误
     *
     * @param e 错误
     * @return json
     */
    @ExceptionHandler(Throwable.class)
    public ResponseEntity<?> exception(Throwable e) {
        saveException(e);
        logger.error("严重错误，从未考虑到的错误范围", e);
        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
        return new ResponseEntity<>(buildErrorMessage(e, status), status);
    }

    /**
     * 其他未考虑到的所有错误
     *
     * @param e 错误
     * @return json
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> exception(Exception e) {
        saveException(e);
        logger.error("严重错误，未捕获的其他异常", e);
        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
        return new ResponseEntity<>(buildErrorMessage(e, status), status);
    }

    /**
     * 客户端中止异常
     *
     * @param e 错误
     * @return json
     */
    @ExceptionHandler(ClientAbortException.class)
    public ResponseEntity<?> clientAbortException(ClientAbortException e) {
        saveException(e);
        logger.error("{} {}?{} 客户端中止异常: {}", request.getMethod(), request.getRequestURI(), request.getQueryString(), e.getMessage());
        return new ResponseEntity<>(null, null, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    /**
     * 空指针错误
     *
     * @param e 错误
     * @return json
     */
    @ExceptionHandler(NullPointerException.class)
    public ResponseEntity<?> nullPointerException(NullPointerException e) {
        saveException(e);
        logger.error("空指针错误", e);
        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
        return new ResponseEntity<>(failed(new IErrorCode() {
            @Override
            public long getCode() {
                return status.value();
            }

            @Override
            public String getMsg() {
                return "空指针错误";
            }
        }), status);
    }

    /**
     * Http请求方法不支持异常，请求一个未定义的 HttpMethod 方法。
     * 例如：
     * <p>定义了 @GetMapping("/user") ，但是使用了 POST、PUT、DELETE 请求了该 URI ，则抛出该异常</p>
     *
     * @param e 错误
     * @return json
     */
    @ExceptionHandler({HttpRequestMethodNotSupportedException.class})
    public ResponseEntity<?> httpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
        saveException(e);
        logger.error("Http请求方法不支持异常", e);
        HttpStatus status = HttpStatus.METHOD_NOT_ALLOWED;
        List<String> message = new ArrayList<>();
        message.add(String.format("URI不支持 %s 请求", e.getMethod()));
        Set<HttpMethod> supportedHttpMethods = e.getSupportedHttpMethods();
        if (supportedHttpMethods != null) {
            String supportedMethods = supportedHttpMethods.stream().map(Enum::name).collect(Collectors.joining("/"));
            message.add(String.format("该URI可能支持 %s 请求", supportedMethods));
        }
        return new ResponseEntity<>(buildErrorMessage(String.join(", ", message), status), status);
    }

    /**
     * WEB 404 错误，不启用 @EnableWebMvc 注解， spring.mvc.throw-exception-if-no-handler-found 配置失效，无法抛出404异常在这里捕获处理。
     * 如果要捕获404错误，请重新继承实现 BasicErrorController 功能
     *
     * @param e 错误
     * @return json
     */
    @ExceptionHandler({NoHandlerFoundException.class})
    public ResponseEntity<?> noHandlerFoundException(NoHandlerFoundException e) {
        saveException(e);
        logger.error("404错误", e);
        HttpStatus status = HttpStatus.NOT_FOUND;
        return new ResponseEntity<>(buildErrorMessage(e, status), status);
    }

    /**
     * WEB 请求类型转换错误，请求的数据类型转换错误异常
     *
     * @param e 错误
     * @return json
     */
    @ExceptionHandler({HttpMessageNotReadableException.class})
    public ResponseEntity<?> httpMessageNotReadableException(HttpMessageNotReadableException e) {
        saveException(e);
        logger.error("数据类型转换错误: {}", e.getLocalizedMessage());
        logger.error("数据类型转换错误", e);
        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
        return new ResponseEntity<>(buildErrorMessage(e, status), status);
    }

    /**
     * WEB 请求数据校验错误（通过 @Validated @RequestBody JSON 校验）。
     *
     * @param e 错误
     * @return json
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<?> methodArgumentNotValidException(MethodArgumentNotValidException e) {
        saveException(e);
        logger.error("请求参数数据校验不通过", e);
        HttpStatus status = HttpStatus.BAD_REQUEST;
        String message = e.getBindingResult().getAllErrors()
                .stream()
                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .collect(Collectors.joining(", "));
        return new ResponseEntity<>(buildErrorMessage(message, status), status);
    }

    /**
     * WEB 请求数据校验错误。使用 @Valid 和 @Validated 校验请求参数数据出现错误（检验不合格）时抛出这个异常（手动抛出）
     *
     * @param e 错误
     * @return json
     */
    @ExceptionHandler(BindException.class)
    public ResponseEntity<?> bindException(BindException e) {
        saveException(e);
        logger.error("请求参数数据校验不通过", e);
        HttpStatus status = HttpStatus.BAD_REQUEST;
        String message = e.getAllErrors()
                .stream()
                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .collect(Collectors.joining(", "));
        return new ResponseEntity<>(buildErrorMessage(message, status), status);
    }

    /**
     * WEB 请求数据校验错误。在 Controller 上加 @Validated 注解，然后在请求方法中给参数加类似 @NotBlank 注解
     *
     * @param e 错误
     * @return json
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<?> constraintViolationException(ConstraintViolationException e) {
        saveException(e);
        logger.error("请求参数数据校验不通过", e);
        HttpStatus status = HttpStatus.BAD_REQUEST;
        String message = e.getConstraintViolations()
                .stream()
                .map(ConstraintViolation::getMessage)
                .collect(Collectors.joining(", "));
        return new ResponseEntity<>(buildErrorMessage(message, status), status);
    }

    /**
     * 权限认证错误
     *
     * @param e 错误
     * @return json
     */
    @ExceptionHandler(AuthenticationException.class)
    public ResponseEntity<?> authenticationException(AuthenticationException e) {
        saveException(e);
        logger.error("权限相关错误", e);
        HttpStatus status = HttpStatus.UNAUTHORIZED;
        return new ResponseEntity<>(buildErrorMessage(e, status), status);
    }

    /**
     * 拒绝访问异常，在访问有权限限制的方法时（@PreAuthorize），如果校验失败则在这里处理异常
     *
     * @param e 错误
     * @return json
     */
    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<?> authenticationException(AccessDeniedException e) {
        saveException(e);
        logger.error("拒绝访问异常", e);
        HttpStatus status = HttpStatus.FORBIDDEN;
        return new ResponseEntity<>(buildErrorMessage(e, status), status);
    }

    /**
     * 构建错误信息
     *
     * @param throwable 抛出的异常
     * @param status    HTTP 状态码
     * @return 错误信息对象
     */
    private R<?> buildErrorMessage(Throwable throwable, HttpStatus status) {
        return failed(new IErrorCode() {
            @Override
            public long getCode() {
                return status.value();
            }

            @Override
            public String getMsg() {
                return throwable.getMessage();
            }
        });
    }

    /**
     * 构建错误信息
     *
     * @param msg    错误信息提示
     * @param status HTTP 状态码
     * @return 错误信息对象
     */
    private R<?> buildErrorMessage(String msg, HttpStatus status) {
        return failed(new IErrorCode() {
            @Override
            public long getCode() {
                return status.value();
            }

            @Override
            public String getMsg() {
                return msg;
            }
        });
    }

    /**
     * 保存异常信息
     *
     * @param throwable 异常
     */
    private void saveException(Throwable throwable) {
        ExceptionLogEntity entity = new ExceptionLogEntity(throwable);
        Principal principal = UserUtils.getPrincipal();
        if (principal != null) {
            entity.setUserId(principal.getName());
        }
        if (request != null) {
            entity.setRequestCode(String.valueOf(request.hashCode()));
            entity.setUri(request.getRequestURI().substring(request.getContextPath().length()));
        }
        exceptionLogService.saveExceptionLog(entity);
    }
}
